﻿// Copyright 2009 William P. Hickey Jr. and William U. Walker
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//     
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//  
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.Threading;
using AgwpePort;
using AgwpePort.Aprs;

namespace CSReporter
{
    #region " Enumeration definitions "

    /// <summary>
    /// Specifies values that describe types of packet interfaces.
    /// </summary>
    public enum PacketControlType
    {
        Disabled = 0,
        AGWPE = 1,
        SerialPort = 2
    }

    #endregion

    public class Radio
    {
        // Static initialization ensures thread safety.
        private static readonly Radio singletonInstance = new Radio();
        private MainForm mainForm;
        private ConfigList configure;
        private AgwpePort.AgwpePort agwpePort;
        private System.Windows.Forms.Timer beaconTimer;
        private string callSign;
        private string hostName;
        private int hostPort;
        private byte radioPort;
        private string digiPath;
        private PacketControlType interfaceType;

        /// <summary>
        /// Gets a boolean value indicating whether or not a connection is currently established with a communications port or TNC interface service.
        /// </summary>
        public bool IsOpen
        {
            get {
                switch (interfaceType)
                {
                    case PacketControlType.AGWPE:
                        return (agwpePort.IsOpen());
                }
                return false;
            }
        }

        /// <summary>
        /// Gets or sets a boolean value indicating whether or not beacon transmissions are enabled.
        /// </summary>
        public bool BeaconEnabled
        {
            get { return (beaconTimer.Enabled); }
            set { beaconTimer.Enabled = value; }
        }

        /// <summary>
        /// Gets or sets the delay, in milliseconds, between beacon transmissions.
        /// </summary>
        /// <value>The default value is 1800000 (30 minutes).</value>
        public int BeaconInterval
        {
            get { return (beaconTimer.Interval); }
            set { beaconTimer.Interval = value; }
        }

        /// <summary>
        /// Gets or sets the callsign of the transmitting station.
        /// </summary>
        public string CallSign
        {
            get { return callSign; }
            set { callSign = value.Trim().ToUpper(); }
        }

        /// <summary>
        /// Gets or sets the DNS machine name or IP address of a machine hosting a TNC interface service, such as AGWPE.
        /// </summary>
        public string HostName
        {
            get { return hostName; }
            set { hostName = value; }
        }

        /// <summary>
        /// Gets or sets the port address of a TNC interface service, such as AGWPE.
        /// </summary>
        public int HostPort
        {
            get { return hostPort; }
            set { hostPort = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating which configured radio, on a TNC interface service, should be used for transmission.
        /// </summary>
        public int RadioPort
        {
            get { return (int)radioPort + 1; }
            set { radioPort = (value >= 1 & value <= 16) ? (byte)(value - 1) : radioPort; }
        }

        /// <summary>
        /// Gets or sets the digipeater digiPath used by beacon transmissions and by messages when no digiPath is supplied.
        /// </summary>
        public string DefaultDigiPath
        {
            get { return digiPath; }
            set { digiPath = value.Trim().ToUpper(); }
        }

        public PacketControlType InterfaceType
        {
            get { return interfaceType; }
            set {
                agwpePort = new AgwpePort.AgwpePort();
                agwpePort.FrameReceived += new AgwpePort.AgwpePort.AgwpeFrameReceivedEventHandler(this.agwpePort_FrameReceived);
                interfaceType = value; 
            }
        }

        protected Radio()
        {
            mainForm = MainForm.GetMainForm();
            // Load data into the ConfigList object.
            configure = ConfigList.GetConfigList();
            beaconTimer = new System.Windows.Forms.Timer();
            beaconTimer.Tick += new System.EventHandler(this.beaconTimer_Tick);
            // Convert config.BeaconInterval to milliseconds.
            this.BeaconInterval = configure[0].BeaconInterval * 60 * 1000;
            this.InterfaceType = configure[0].InterfaceType;
            this.CallSign = configure[0].CallSign.Trim().ToUpper();
            this.HostName = configure[0].AgwHost;
            this.HostPort = configure[0].AgwPort;
            this.RadioPort = configure[0].AgwVhfUhfRadio;
            this.DefaultDigiPath = configure[0].DigiPath;

            switch (interfaceType)
            {
                case PacketControlType.AGWPE:
                    agwpePort = new AgwpePort.AgwpePort();
                    agwpePort.FrameReceived += new AgwpePort.AgwpePort.AgwpeFrameReceivedEventHandler(this.agwpePort_FrameReceived);
                    break;               
            }
        }

        public static Radio GetRadio()
        {
            return singletonInstance;
        }

        public bool SendMessage(int startDelay, int loopDelay, string toCall, string viaList, string message, string aprsMsgNumber)
        {
            AgwpeMessage msg;
            Thread thread;

            if (message == null | toCall == null | viaList == null)
                return false;

            // Make sure we are connected to an AGWPE process and all required data is reasonably valid.
            if (message.Trim().Length > 0 & toCall.Trim().Length > 0 & callSign.Trim().Length > 0)
            {
                msg = new AgwpeMessage(agwpePort, startDelay, loopDelay, callSign.Trim(), toCall.ToUpper(), viaList.ToUpper(), ASCIIEncoding.UTF8.GetBytes(message.Trim()));
                msg.AprsMsgNumber = aprsMsgNumber;
                thread = new Thread(msg.SendMessage);
                thread.Start();
                return (true);
            }
            return (false);
        }

        public bool SendAck(string toCall, string viaList, string aprsMsgNumber)
        {
            // If the message or toCall is null, go no further.
            if (aprsMsgNumber == null | toCall == null)
                return (false);

            // Make sure we are connected to an AGWPE process and all required data is reasonably valid.
            if (agwpePort.IsOpen() & toCall.Trim().Length > 0 & callSign.Trim().Length > 0)
            {
                // Send the acknowledge message.
                agwpePort.SendUnproto(agwpePort.RadioPort, callSign.Trim(), toCall.Trim(), ASCIIEncoding.UTF8.GetBytes(":" + toCall.PadRight(9, ' ') + ":ack" + aprsMsgNumber.Trim()), viaList.Split(','));
                return (true);
            }
            return (false);
        }

        public bool SendCSReport(CSReportMessage csReport)
        {
            if (configure[0].ReportToCall.Length > 0)
            {
                return SendCSReport(configure[0].ReportToCall, configure[0].DigiPath, csReport.Station, csReport.ReportID.ToString().PadLeft(4, '0'),
                    AprsCalc.TimeStampToDateTime(csReport.IncidentTime), csReport.Latitude, csReport.Longitude, csReport.StatusCode,
                    csReport.CityCode, csReport.StateCode, csReport.StreetNumber, csReport.StreetDirection, csReport.StreetName,
                    csReport.CrossRoad1, csReport.CrossRoad2, csReport.DetailText);
            }
            return false;
        }

        public bool SendCSReport(string toCall, string viaList, CSReportMessage csReport)
        {
            if (configure[0].ReportToCall.Length > 0)
            {
                return SendCSReport(toCall, viaList, csReport.Station, csReport.ReportID.ToString().PadLeft(4, '0'),
                    AprsCalc.TimeStampToDateTime(csReport.IncidentTime), csReport.Latitude, csReport.Longitude, csReport.StatusCode,
                    csReport.CityCode, csReport.StateCode, csReport.StreetNumber, csReport.StreetDirection, csReport.StreetName,
                    csReport.CrossRoad1, csReport.CrossRoad2, csReport.DetailText);
            }
            return false;
        }

        public bool SendCSReport(string toCall, string viaList, string station, string reportId, DateTime incidentTime, string latitude, string longitude,
            string statusCode, string cityCode, string stateCode, string streetNumber, string streetDirection, string streetName,
            string crossRoad1, string crossRoad2, string detailText)
        {
            string aprsIncidentTime = incidentTime.ToUniversalTime().Day.ToString().PadLeft(2, '0') +
                                      incidentTime.ToUniversalTime().Hour.ToString().PadLeft(2, '0') +
                                      incidentTime.ToUniversalTime().Minute.ToString().PadLeft(2, '0') + "z";
            DateTime curTime = DateTime.Now;
            string aprsMsgNumber;

            if (IsOpen & toCall.Trim().Length > 0 & station.Trim().Length > 0 & reportId.Trim().Length > 0 & latitude.Length == 8 & longitude.Length == 9 & statusCode.Trim().Length > 0 & statusCode.Trim().Length <= 22 & incidentTime >= DateTime.Now.AddHours(-24) & incidentTime <= DateTime.Now)
            {
                aprsMsgNumber = String.Format("{0:X}", (curTime.Hour * 10000) + (curTime.Minute * 1000) + (curTime.Second));
                string statusReport1 = ":" + toCall.Trim().ToUpper().PadRight(9, ' ') + ":^1" + station.Trim().ToUpper().PadRight(10, ' ') + reportId.Trim().ToUpper().PadLeft(4, '0') + aprsIncidentTime + latitude + longitude + statusCode.Trim().ToUpper().PadRight(22, ' ') + cityCode.Trim().ToUpper().PadRight(3, ' ') + stateCode.Trim().ToUpper().PadRight(2, ' ') + "{" + aprsMsgNumber;
                SendMessage(0, 15, toCall.Trim().ToUpper(), viaList.Trim().ToUpper(), statusReport1, aprsMsgNumber);

                if (IsOpen & (streetNumber.Trim().Length > 0 | streetDirection.Trim().Length > 0 | streetName.Trim().Length > 0))
                {
                    aprsMsgNumber = String.Format("{0:X}", (curTime.Hour * 10000) + (curTime.Minute * 1000) + (curTime.Second + 1));
                    string statusReport2 = ":" + toCall.Trim().ToUpper().PadRight(9, ' ') + ":^2" + station.Trim().ToUpper().PadRight(10, ' ') + reportId.Trim().ToUpper().PadLeft(4, '0') + streetNumber.Trim() + "," + streetDirection.Trim() + "," + streetName.Trim() + "{" + aprsMsgNumber;
                    SendMessage(3, 15, toCall.Trim().ToUpper(), viaList.Trim().ToUpper(), statusReport2, aprsMsgNumber);
                }

                if (IsOpen & (crossRoad1.Trim().Length > 0 | crossRoad2.Trim().Length > 0))
                {
                    aprsMsgNumber = String.Format("{0:X}", (curTime.Hour * 10000) + (curTime.Minute * 1000) + (curTime.Second + 2));
                    string statusReport3 = ":" + toCall.Trim().ToUpper().PadRight(9, ' ') + ":^3" + station.Trim().ToUpper().PadRight(10, ' ') + reportId.Trim().ToUpper().PadLeft(4, '0') + crossRoad1.Trim() + "," + crossRoad2.Trim() + "{" + aprsMsgNumber;
                    SendMessage(6, 15, toCall.Trim().ToUpper(), viaList.Trim().ToUpper(), statusReport3, aprsMsgNumber);
                }

                if (IsOpen & detailText.Trim().Length > 0)
                {
                    // Replace new line characters with a space.
                    detailText = detailText.Replace("\n", " ");
                    // Remove all other non-printable characters.
                    detailText = Regex.Replace(detailText, "[\u0000-\u001F\u0080-\u00FF]", "");
                    // Determine how many 67 byte messages must be sent.
                    int segments = (int)Math.Ceiling((double)detailText.Trim().Length / 51);

                    if (segments > 0)
                    {
                        string[] message = new string[segments];
                        string[] msgIds = new string[segments];

                        for (int index = 0; index < segments; index++)
                        {
                            // Format the message.
                            message[index] = ":" + toCall.Trim().ToUpper().PadRight(9, ' ') + ":^C" + station.Trim().ToUpper().PadRight(10, ' ') + reportId.Trim().ToUpper().PadLeft(4, '0') + detailText.Trim().Substring((index * 51), Math.Min(51, detailText.Trim().Length - (index * 51)));
                            //":" + toTextBox.Text.Trim().PadRight(9, ' ') + ":" + detailText.Trim().Substring((index * 67), Math.Min(67, detailText.Trim().Length - (index * 67)));
                            // Create the message id string.
                            msgIds[index] = String.Format("{0:X}", (curTime.Hour * 10000) + (curTime.Minute * 1000) + (curTime.Second + 3 + index));
                            // Append the message id delimiter and the message id to the end of the message.
                            message[index] += "{" + msgIds[index];
                            // Send the message.
                            if (IsOpen)
                                SendMessage(9 + (3 * index), 15, toCall.Trim().ToUpper(), viaList.Trim().ToUpper(), message[index], msgIds[index]);
                        }
                    }
                }
                return (true);
            }
            return (false);
        }

        public bool SendBeacon()
        {
            string beacon = "";
            DateTime dt = DateTime.UtcNow;
            string timeStamp = dt.Day.ToString().PadLeft(2, '0') +
                               dt.Hour.ToString().PadLeft(2, '0') +
                               dt.Minute.ToString().PadLeft(2, '0') + "z";

            // Start building the beacon message.
            // If the latitude and longitude of this station are known...
            if (configure[0].Latitude != 0 & configure[0].Longitude != 0)
            {
                // Build a position report with timestamp.
                // The use of the "@" data type identifier indicates that we are APRS message capable.
                beacon = "@" + timeStamp +
                    (new Coordinate(configure[0].Latitude)).ToAprsLatString() + "/" +
                    (new Coordinate(configure[0].Longitude)).ToAprsLonString() + "-" +
                    "PHG" + configure[0].TxPower.ToString() +
                    configure[0].Haat.ToString() +
                    configure[0].AntGain.ToString() +
                    configure[0].AntDirectivity.ToString() +
                    configure[0].BeaconText + "\r";
            }
            else
            {
                // Otherwise build a simple station status message.
                beacon += ">" + timeStamp + configure[0].BeaconText + "\r";
            }

            // Make sure we are connected to an AGWPE process and all required data is reasonably valid.
            if (agwpePort.IsOpen() & callSign.Trim().Length > 0 & beacon.Length > 0 & radioPort >= 0 & radioPort <= 15)
            {
                // Send unproto beacon.
                agwpePort.SendUnproto(radioPort, callSign.Trim(), "BEACON", ASCIIEncoding.ASCII.GetBytes(beacon), digiPath.Split(','));
                return (true);
            }
            return (false);
        }

        public bool Connect()
        {
            switch (interfaceType)
            {
                case PacketControlType.AGWPE:
                    if (!agwpePort.IsOpen() & callSign.Length > 0 & hostName.Length > 0)
                    {
                        try
                        {
                            // Open a connection to AGWPE.
                            agwpePort.Open(radioPort, hostName, hostPort);
                            // Register a callsign that we can accept incoming ax.25 connection requests for.
                            agwpePort.RegisterCallSign(callSign);
                            // Request the AGWPE major and minor version numbers.
                            agwpePort.GetVersion();
                            // Request the capability information for the selected radio port.
                            agwpePort.GetPortCapability(radioPort);
                            // Request information for all configured radio ports.
                            agwpePort.GetPortInfo();
                            // Start monitoring ax.25 traffic.
                            agwpePort.StartMonitoring();
                            return (true);
                        }
                        catch (Exception ex)
                        {
                            MessageBox.Show(ex.Message);
                            if (agwpePort.IsOpen())
                            {
                                beaconTimer.Enabled = false;
                                agwpePort.Close();
                            }
                        }
                    }
                    break;
            }
            return (false);
        }

        public void Close()
        {
            switch (interfaceType)
            {
                case PacketControlType.AGWPE:
                    if (agwpePort.IsOpen())
                    {
                        beaconTimer.Enabled = false;
                        agwpePort.Close();
                    }
                    break;
            }
        }

        private void agwpePort_FrameReceived(object sender, AgwpePort.AgwpeEventArgs e)
        {
            // Determine the kind of data that is associated with this AGWPE frame.
            switch (e.FrameHeader.DataKind)
            {
                case (byte)'X': // Register call sign
                    mainForm.UpdateConnectButton();
                    break;
                case (byte)'G': // Get port information
                    break;
                case (byte)'g': // Get port capability
                    break;
                case (byte)'R': // Get version
                    break;
                case (byte)'T': // Monitor our own transmissions.
                    mainForm.Record(new AgwpeMoniUnproto(((AgwpeAX25Data)e.FrameData).AX25Data, ((AgwpeAX25Data)e.FrameData).AX25Data.Length));
                    break;
                case (byte)'U': // Read AX25 Unproto frames and data
                    mainForm.Record((AgwpeMoniUnproto)e.FrameData);
                    break;
            }
        }

        private void beaconTimer_Tick(object sender, EventArgs e)
        {
            SendBeacon();
        }
    }
}
